第 4 课:P2P 网络
在我们上面讲到的 IP 访问和域名访问,使用的都是 本地设备+服务器(Client-Server) 的架构。一台中心服务器处理多个本地设备的访问请求,因此是属于中心化的网络结构。
而 P2P(Peer-to-Peer,点对点连接)网络通信模型,强调每个节点(peer)地位平等、直接通信,不依赖中心服务器。
P2P = 一台设备直接连接另一台设备,进行数据传输
与传统的“客户端-服务器”(Client-Server)架构不同,P2P 没有中心节点。每个 peer 都是“客户端 + 服务器”的双重身份。

| 模型 | 特点 |
|---|---|
| Client-Server | 所有请求都通过中心服务器转发(如网站、游戏服务器) |
| P2P | 每个节点之间可以直接通信(如 BT、区块链) |
应用例子如:
| 应用场景 | 说明 |
|---|---|
| BitTorrent | 下载文件时,你下载别人的同时也在上传给别人(分布式) |
| 区块链 | 比特币/以太坊等所有节点直接同步数据,无中心服务器 |
| WebRTC | 浏览器之间的视频聊天、屏幕共享(如 Google Meet 的底层) |
| 游戏联机 | 有些局域网或小型在线游戏是玩家之间直接连接 |
虽然理念简单,但实际应用时主要要解决三个技术难点:
-
大多数用户都在 NAT/CGNAT 后面,没有公网 IP,如何互相直接连接?
-
没有中心验证机制 → 极易伪造、污染数据?
-
Peer 可能随时上线下线,如何保证可用性?
让我们依次来解答这些问题
(一)连接建立流程
先来看下图中,设备 A 如何和设备 C 建立 P2P 连接

设备 A 和设备 C 分别位于两个不同 NAT 后面的局域网中
- 设备 A: IP 为
192.168.0.2,通过路由器1(NAT1,公网出口100.64.0.3)访问外网。 - 设备 C: IP 为
192.168.0.2,通过路由器2(NAT2,公网出口100.64.0.4)访问外网。 - ISP: 两个 NAT 的上游 ISP 地址为
100.64.0.1,ISP 公网 IP 是142.142.142.142 - 公共服务器: IP 为
3.152.6.45,部署了 P2P 协商服务(信令服务器 + STUN + TURN)
因为 A 和 C 都在 NAT 后面,互相不知道对方的真实出口 IP:port,不能直接向对方发送 UDP/TCP 数据包,因此必须借助一个中立的中继服务器,称为 Signaling Server。
注意:Signaling Server 只是传话筒,不参与后续数据传输。
步骤一:注册身份
首先 设备A 要在 Signaling Server 上注册身份(类似登录),A 会发送:
{
"type": "register",
"user": "deviceA"
}
Signaling Server 回复“注册成功”:
{
"type": "register-success",
"user": "deviceA"
}
此时,A 已注册为 "deviceA",等待匹配。于是 A 向服务器发起连接请求:
{
"type": "connect-request",
"from": "deviceA",
"to": "deviceC"
}
步骤二:连接协商
假设 设备 C 也在这台服务器上注册过了,于是 Signaling Server 将 A 的请求转发给 C:
{
"type": "incoming-request",
"from": "deviceA"
}
C 收到该请求后,回复 Signaling Server C ,表示同意连接
{
"type": "connect-accept",
"from": "deviceC",
"to": "deviceA"
}
步骤三:交换连接方式
现在 A 知道 C 已经答应了连接请求,那么就需要告诉 C 接下来自己通信准备采用的方式,例如使用的编码格式,加密方式等等。这个其实叫做 SDP(Session Description Protocol) ,包含了编解码器(H.264, Opus等)、媒体类型(audio, video)、ICE 候选(Candidate 地址)、DTLS 加密参数等。A 发送 SDP offer,经过 Signaling Server 转发,发送给 C
{
"type": "sdp-offer",
"from": "deviceA",
"to": "deviceC",
"sdp": {
"type": "offer",
"sdp": "v=0\no=alice 2890844526 2890844526 IN IP4 192.0.2.1\ns=-\nt=0 0\n..."
}
}
C 收到 offer,发送 SDP answer
{
"type": "sdp-answer",
"from": "deviceC",
"to": "deviceA",
"sdp": {
"type": "answer",
"sdp": "v=0\no=bob 283903 283903 IN IP4 192.0.2.2\ns=-\nt=0 0\n..."
}
}
此时 A、C 双方都知道彼此支持的媒体格式和连接方式。
步骤四:交换连接渠道
SDP(Session Description Protocol) 中有一条重要的信息叫做 ICE 候选(Candidate 地址),这是指 A、C 除了通过这台 Signaling Server 中继之外,还可以通过什么渠道互相连接。例如:
设备 A 计划使用 733 端口进行P2P通信,则之前就已经用 733 端口向 Signaling Server 发起 Websocket 连接,同时
192.168.0.2:733被家庭路由器 NAT 映射为100.64.0.3:65000,由进一步被 ISP 路由器 NAT映射为142.142.142.142:89000,流程如下。`192.168.0.2:733` <--> `100.64.0.3:65000` <--> `142.142.142.142:89000` <--> `3.152.6.45`
那么接下来 A 发给 C 的是:142.142.142.142:89000,以后 C 就可以用 这个地址直接联系到 A,不需要通过中间服务器。
这种连接方式也称为 NAT 打洞,或者 UDP 打洞,或者 STUN 打洞
事实上, A 给 C 的连接方式不仅只有这种 NAT 映射路径,还有其他连接方式,它们都是 ICE 候选(Candidate 地址)
ICE Candidate是 ICE 协议中的一个关键概念,它描述了“某个设备可能被连接到的网络路径和地址”。一个 candidate 就像是对外广播的名片:“我可以通过这个地址被别人连接!”
ICE 协议(Interactive Connectivity Establishment)会收集所有可能连接的方式(如 NAT 外部端口、STUN地址、Host地址等)
所以 Candidate 的“种类”有三个:
| 类型 | 说明 | 条件 | 代价 | 举例 |
|---|---|---|---|---|
| Host | 内网地址 | AC 处于相同子网 | 低 | 192.168.0.2:733 |
| Server Reflexive | 经过 NAT 映射后的公网地址 | NAT 支持对称打洞 | 中 | 142.142.142.142:89000 |
| Relay | 通过 TURN 中继服务器 | 永远可以 | 高 | relay.turnserver.com:54789 |
因此 A 给 C 的 ICE Candidate 为:
# Host Candidate(低代价,内网地址)
{
"type": "ice-candidate",
"from": "deviceA",
"to": "deviceC",
"candidate": {
"candidate": "candidate:842163049 1 udp 1677729535 192.168.0.2 733 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
# Server Reflexive Candidate(中代价,NAT 映射地址)
{
"type": "ice-candidate",
"from": "deviceA",
"to": "deviceC",
"candidate": {
"candidate": "candidate:123456789 1 udp 1677724415 142.142.142.142 89000 typ srflx raddr 192.168.0.2 rport 733",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
# Relay Candidate(高代价,TURN 中继)
{
"type": "ice-candidate",
"from": "deviceA",
"to": "deviceC",
"candidate": {
"candidate": "candidate:987654321 1 udp 1677719551 relay.turnserver.com 54789 typ relay raddr 192.168.0.2 rport 733",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
解释:
-
candidate:842163049是一个 foundation 和 component ID,可保持不变 -
typ host表示这是 host 类型 -
typ srflx表示这是一个 Server Reflexive 类型 -
typ relay表示中继类型 -
raddr/rport表示此 candidate 原始来源地址是内网的192.168.0.2:733 -
relay.turnserver.com 54789是通过 TURN 中继服务器获取的转发地址
在 C 拿到 A 的 ICE Candidate 后,就会去维护一个类似这样的列表:
[
{
"type": "host",
"candidate": "192.168.0.2:733"
},
{
"type": "srflx", // server reflexive
"candidate": "142.142.142.142:89000"
},
{
"type": "relay", // TURN 中继地址(可选)
"candidate": "23.56.1.10:45789"
}
]
依次尝试去连接
- 首选尝试 host ↔ host(在局域网内可行)
- 不行再试 srflx ↔ srflx(NAT打洞)
- 最后保底用 relay ↔ relay(必须中继)
这就是 **ICE 优选(ICE connectivity checks)**的过程。
至此为止,A 和 C 均掌握对方的候选地址。接下来,使用 ICE 协议进行连接测试与优选,并建立 DTLS 安全通道。Signaling Server 至此退出,不再参与通信。
ICE Candidate **注意,Signaling Server 只负责传话,不负责记录,因此A 的 NAT 穿透地址等 连接渠道不是 Signaling Server记录的,而是作为一项数据直接写在 A 发给 Signaling Server 的数据包中。那 A 是怎么知道自己的 NAT 出口地址的呢?
- 设备 A 通过本地网卡拿到 host candidate(如
192.168.0.2:733) - A 再向 STUN 服务器发送请求,它会返回 NAT 映射地址
142.142.142.142:89000 - A 可能还会连接 TURN 服务器获得 relay 地址(可选)
STUN(Session Traversal Utilities for NAT)服务器告诉客户端:你“看起来”来自什么公网 IP 和端口,例如你向一个 STUN 服务器(比如 stun.l.google.com:19302)发一个包,对方回复说:
“你在我这看来是从
142.142.142.142:89000发过来的”
你就知道了你的 NAT 映射地址,可以把它作为 ICE Candidate 发给通信对方。
(二) NAT 的穿透难度
Server Reflexive 通信渠道并不是一直可用的。不同的 NAT 对穿透支持程度不同,分以下几种类型
| NAT 类型 | 行为特征 | 穿透性(UDP打洞) | 示例行为 |
|---|---|---|---|
| Full Cone NAT | 内部 IP+端口 映射后,任何外部主机都可以通过这个映射地址联系到你 | ✅ 非常容易 | 192.168.0.2:733 ➝ 1.2.3.4:9000,任何人只要知道 1.2.3.4:9000 都能连你 |
| Restricted Cone NAT | 只有你主动访问过的目标 IP 才能回连你 | ⚠️ 可穿透 | 如果你发了 UDP 给 3.3.3.3,那么只有 3.3.3.3 可以访问你 |
| Port Restricted Cone NAT | 只有你主动访问的 IP+端口 才能连你 | ⚠️ 穿透更难 | 比如你访问了 3.3.3.3:5000,则只接受这个地址 |
| Symmetric NAT | 每一个外部目标(IP+端口)都会触发不同的 NAT 映射端口 | ❌ 无法打洞,只能用 TURN | 访问 3.3.3.3:5000 时被分配的NAT出口是 1.2.3.4:9000,访问 3.3.3.3:6000 是时被分配的 NAT 出口是1.2.3.4:9001,候选地址无法通用 |
例如一台在 Symmetric NAT 环境中的机器,通过访问 STUN 服务器 3.3.3.3:1234,A 知道了的自己的 NAT 公网出口是 143.231.34.12:4321。但是问题是,143.231.34.12:4321 只允许来自 3.3.3.3:1234 的连接,而不允许来自 C 的 NAT 公网出口 177.3.45.32:1145的连接,因此 C 无法用 NAT 打洞连接上 A。
而对于 Port Restricted Cone NAT 或 Restricted Cone NAT,并不完全如此。他们可以通过这种方式建立联系:
- A 使用 STUN 服务器获取到了
143.231.34.12:4321。 - C 也获取了自己的
srflx地址155.66.77.88:5678。 - A 向 C 的地址发送一个探测包
- 此时,A 的 NAT 将允许从
155.66.77.88:5678发来的返回包。 - 同理,C 向 A 发包,A 的 NAT 也打开 pinhole。
- 如果两边时机对上,打洞成功!
这正是 WebRTC + ICE 的核心原理之一:双向并发发包,尽量在 NAT 映射短时间内交汇。
而 Symmetric NAT 之所以不能用这个方法,是因为 A 使用 STUN 服务器获取到的 NAT 出口 143.231.34.12:4321仅对STUN 服务器 有效。A 去向 C 的地址发包的时候,实际上被分配了一个新端口(如143.231.34.12:4444),而通过中继服务器发给 C 的连接地址其实是 143.231.34.12:4321,因此 C 不能连接上 A。
大部分家庭宽带或校园网都是 Port Restricted 或 Symmetric NAT。这就是为什么在校园网或者家庭 IP 中经常无法 P2P 连接。
想要知道自己处于什么环境,可以 使用 Trickle ICE ,这是一个 WebRTC 的网页测试工具,打开后输入 ICE 服务器:
stun:stun.l.google.com:19302
点击 “Gather candidates”,观察候选地址(candidates)类型
- 若只出现
host,说明你在同一网段或局域网(未穿透 NAT) - 出现
srflx:说明 STUN 成功,你不是 Symmetric NAT(可能是 Full Cone 或 Restricted) - 若只出现
relay才能通信,说明是 Symmetric NAT(需要 TURN)

如果你希望更专业一点,可以在 Linux/Mac 下使用开源工具测试:
git clone https://github.com/mystfit/nat-type-collector.git
cd nat-type-collector
go build # 注意:需安装 Go 环境
./nat-type-collector
还有一个问题,STUN 是如何判断 NAT 类型的?
STUN 协议中通过发送三种 Binding Request 来测试 NAT 类型:
| 测试编号 | 请求行为 | 判断依据 |
|---|---|---|
| Test I | 普通 Binding Request | 是否返回公共 IP |
| Test II | 更改来源地址和端口 | 判断是否是 Full Cone |
| Test III | 更改目的 IP | 判断是否是 Symmetric |
通过这三次测试组合,可以判断你的 NAT 类型。
(三)BitTorrent 点对点连接
上面我们提到,磁力链接下载也是一种 P2P 连接。